home *** CD-ROM | disk | FTP | other *** search
/ Chip 2006 June / CHIP 2006-06.2.iso / program / freeware / Democracy-0.8.2.exe / xulrunner / python / feed.py < prev    next >
Encoding:
Python Source  |  2006-04-10  |  60.5 KB  |  1,782 lines

  1. from downloader import grabURL
  2. from HTMLParser import HTMLParser,HTMLParseError
  3. import xml
  4. from urlparse import urlparse, urljoin
  5. from urllib import urlopen
  6. from database import defaultDatabase
  7. from item import *
  8. from scheduler import ScheduleEvent
  9. from copy import copy
  10. from xhtmltools import unescape,xhtmlify,fixXMLHeader, fixHTMLHeader, toUTF8Bytes, urlencode
  11. from cStringIO import StringIO
  12. from threading import Thread, Semaphore
  13. import traceback #FIXME get rid of this
  14. from datetime import datetime, timedelta
  15. from inspect import isfunction
  16. from new import instancemethod
  17. import os
  18. import config
  19. import re
  20. import app
  21.  
  22. whitespacePattern = re.compile(r"^[ \t\r\n]*$")
  23.  
  24. def defaultFeedIconURL():
  25.     import resource
  26.     return resource.url("images/feedicon.png")
  27.  
  28. # Notes on character set encoding of feeds:
  29. #
  30. # The parsing libraries built into Python mostly use byte strings
  31. # instead of unicode strings.  However, sometimes they get "smart" and
  32. # try to convert the byte stream to a unicode stream automatically.
  33. #
  34. # What does what when isn't clearly documented
  35. #
  36. # We use the function toUTF8Bytes() to fix those smart conversions
  37. #
  38. # If you run into Unicode crashes, adding that function in the
  39. # appropriate place should fix it.
  40.  
  41. # Universal Feed Parser http://feedparser.org/
  42. # Licensed under Python license
  43. import feedparser
  44.  
  45. # Pass in a connection to the frontend
  46. def setDelegate(newDelegate):
  47.     global delegate
  48.     delegate = newDelegate
  49.  
  50. # Pass in a feed sorting function 
  51. def setSortFunc(newFunc):
  52.     global sortFunc
  53.     sortFunc = newFunc
  54.  
  55. #
  56. # Adds a new feed using USM
  57. def addFeedFromFile(file):
  58.     d = feedparser.parse(file)
  59.     if d.feed.has_key('links'):
  60.         for link in d.feed['links']:
  61.             if link['rel'] == 'start':
  62.                 generateFeed(link['href'])
  63.                 return
  64.     if d.feed.has_key('link'):
  65.         addFeedFromWebPage(d.feed.link)
  66.  
  67. #
  68. # Adds a new feed based on a link tag in a web page
  69. def addFeedFromWebPage(url):
  70.     feedURL = getFeedURLFromWebPage(url)
  71.     if not feedURL is None:
  72.         generateFeed(feedURL)
  73.  
  74. def getFeedURLFromWebPage(url):
  75.     data = ''
  76.     info = grabURL(url,"GET")
  77.     if info is None:
  78.         return None
  79.     try:
  80.         data = info['file-handle'].read()
  81.         info['file-handle'].close()
  82.     except:
  83.         pass
  84.     return HTMLFeedURLParser().getLink(info['updated-url'],data)
  85.  
  86. # URL validitation and normalization
  87. def validateFeedURL(url):
  88.     return re.match(r"^(http|https|feed)://[^/].*", url) is not None
  89.  
  90. def normalizeFeedURL(url):
  91.     # Valid URL are returned as-is
  92.     if validateFeedURL(url):
  93.         return url
  94.     
  95.     # Check valid schemes with invalid separator
  96.     match = re.match(r"^(http|https|feed):/*(.*)$", url)
  97.     if match is not None:
  98.         return "%s://%s" % match.group(1,2)
  99.  
  100.     # Replace invalid schemes by http
  101.     match = re.match(r"^(.*:/*)*(.*)$", url)
  102.     if match is not None:
  103.         return "http://%s" % match.group(2)
  104.  
  105.     # We weren't able to normalize
  106.     print "DTV: unable to normalize URL %s" % url
  107.     return url
  108.  
  109. ##
  110. # Generates an appropriate feed for a URL
  111. #
  112. # @param url The URL of the feed
  113. def generateFeed(url,ufeed):
  114.     thread = Thread(target=lambda: _generateFeed(url,ufeed), \
  115.                     name="generateFeed -- %s" % url)
  116.     thread.setDaemon(False)
  117.     thread.start()
  118.  
  119. def _generateFeed(url, ufeed, visible=True):
  120.     if (url == "dtv:directoryfeed"):
  121.         return DirectoryFeedImpl(ufeed)
  122.     elif (url == "dtv:search"):
  123.         return SearchFeedImpl(ufeed)
  124.     elif (url == "dtv:searchDownloads"):
  125.         return SearchDownloadsFeedImpl(ufeed)
  126.  
  127.     info = grabURL(url,"GET")
  128.     if info is None:
  129.         return None
  130.     try:
  131.         modified = info['last-modified']
  132.     except KeyError:
  133.         modified = None
  134.     try:
  135.         etag = info['etag']
  136.     except KeyError:
  137.         etag = None
  138.     #Definitely an HTML feed
  139.     if (info['content-type'].startswith('text/html') or 
  140.         info['content-type'].startswith('application/xhtml+xml')):
  141.         #print "Scraping HTML"
  142.         html = info['file-handle'].read()
  143.         if info.has_key('charset'):
  144.             html = fixHTMLHeader(html,info['charset'])
  145.             charset = info['charset']
  146.         else:
  147.             charset = None
  148.         info['file-handle'].close()
  149.         if delegate.isScrapeAllowed(url):
  150.             return ScraperFeedImpl(info['updated-url'],initialHTML=html,etag=etag,modified=modified, charset=charset, visible=visible, ufeed=ufeed)
  151.         else:
  152.             return None
  153.  
  154.     #It's some sort of feed we don't know how to scrape
  155.     elif (info['content-type'].startswith('application/rdf+xml') or
  156.           info['content-type'].startswith('application/atom+xml')):
  157.         #print "ATOM or RDF"
  158.         html = info['file-handle'].read()
  159.         info['file-handle'].close()
  160.         if info.has_key('charset'):
  161.             xmldata = fixXMLHeader(html,info['charset'])
  162.         else:
  163.             xmldata = html
  164.         return RSSFeedImpl(info['updated-url'],initialHTML=xmldata,etag=etag,modified=modified, visible=visible, ufeed=ufeed)
  165.     # If it's not HTML, we can't be sure what it is.
  166.     #
  167.     # If we get generic XML, it's probably RSS, but it still could be
  168.     # XHTML.
  169.     #
  170.     # application/rss+xml links are definitely feeds. However, they
  171.     # might be pre-enclosure RSS, so we still have to download them
  172.     # and parse them before we can deal with them correctly.
  173.     elif (info['content-type'].startswith('application/rss+xml') or
  174.           info['content-type'].startswith('application/podcast+xml') or
  175.           info['content-type'].startswith('text/xml') or 
  176.           info['content-type'].startswith('application/xml')):
  177.         #print " It's doesn't look like HTML..."
  178.         html = info["file-handle"].read()
  179.         info["file-handle"].close()
  180.         if info.has_key('charset'):
  181.             xmldata = fixXMLHeader(html,info['charset'])
  182.             html = fixHTMLHeader(html,info['charset'])
  183.             charset = info['charset']
  184.         else:
  185.             xmldata = html
  186.             charset = None
  187.         try:
  188.             parser = xml.sax.make_parser()
  189.             parser.setFeature(xml.sax.handler.feature_namespaces, 1)
  190.             handler = RSSLinkGrabber(info['redirected-url'],charset)
  191.             parser.setContentHandler(handler)
  192.             parser.parse(StringIO(xmldata))
  193.         except xml.sax.SAXException: #it doesn't parse as RSS, so it must be HTML
  194.             #print " Nevermind! it's HTML"
  195.             if delegate.isScrapeAllowed(url):
  196.                  return ScraperFeedImpl(info['updated-url'],initialHTML=html,etag=etag,modified=modified, charset=charset, visible=visible, ufeed=ufeed)
  197.             else:
  198.                  return None
  199.         except UnicodeDecodeError:
  200.             print "Unicode issue parsing... %s" % xmldata[0:300]
  201.             traceback.print_exc()
  202.             return None
  203.         if handler.enclosureCount > 0 or handler.itemCount == 0:
  204.             #print " It's RSS with enclosures"
  205.             return RSSFeedImpl(info['updated-url'],initialHTML=xmldata,etag=etag,modified=modified, visible=visible, ufeed=ufeed)
  206.         else:
  207.             #print " It's pre-enclosure RSS"
  208.             if delegate.isScrapeAllowed(url):
  209.                 return ScraperFeedImpl(info['updated-url'],initialHTML=xmldata,etag=etag,modified=modified, charset=charset, visible=visible, ufeed=ufeed)
  210.             else:
  211.                 return None
  212.     else:
  213.         print "DTV doesn't know how to deal with "+info['content-type']+" feeds"
  214.         return None
  215.  
  216. ##
  217. # Handle configuration changes so we can update feed update frequencies
  218.  
  219. def configDidChange(key, value):
  220.     if key is config.CHECK_CHANNELS_EVERY_X_MN.key:
  221.         for feed in app.globalViewList['feeds']:
  222.             updateFreq = 0
  223.             try:
  224.                 updateFreq = feed.parsed["feed"]["ttl"]
  225.             except:
  226.                 pass
  227.             feed.setUpdateFrequency(updateFreq)
  228.  
  229. config.addChangeCallback(configDidChange)
  230.  
  231. ##
  232. # Actual implementation of a basic feed.
  233. class FeedImpl:
  234.     def __init__(self, url, ufeed, title = None, visible = True):
  235.         self.available = 0
  236.         self.unwatched = 0
  237.         self.url = url
  238.         self.ufeed = ufeed
  239.         self.items = []
  240.         if title == None:
  241.             self.title = url
  242.         else:
  243.             self.title = title
  244.         self.created = datetime.now()
  245.         self.autoDownloadable = ufeed.initiallyAutoDownloadable
  246.         if self.autoDownloadable:
  247.             self.startfrom = datetime.min
  248.         else:
  249.             self.startfrom = datetime.max
  250.         self.getEverything = False
  251.         self.maxNew = -1
  252.         self.fallBehind = -1
  253.         self.expire = "system"
  254.         self.visible = visible
  255.         self.updating = False
  256.         self.lastViewed = datetime.min
  257.         self.thumbURL = defaultFeedIconURL()
  258.         self.updateFreq = config.get(config.CHECK_CHANNELS_EVERY_X_MN)*60
  259.  
  260.     # Sets the update frequency (in minutes). 
  261.     # - A frequency of -1 means that auto-update is disabled.
  262.     def setUpdateFrequency(self, frequency):
  263.         if frequency < 0:
  264.             self.cancelUpdateEvents()
  265.             self.updateFreq = -1
  266.         else:
  267.             newFreq = max(config.get(config.CHECK_CHANNELS_EVERY_X_MN),
  268.                           frequency)*60
  269.             if newFreq != self.updateFreq:
  270.                 self.updateFreq = newFreq
  271.                 self.scheduleUpdateEvents(-1)
  272.  
  273.     def scheduleUpdateEvents(self, firstTriggerDelay):
  274.         self.cancelUpdateEvents()
  275.         if self.updateFreq > 0:
  276.             self.scheduler = ScheduleEvent(self.updateFreq, self.update)
  277.             if firstTriggerDelay >= 0:
  278.                 ScheduleEvent(firstTriggerDelay, self.update, False)
  279.  
  280.     def cancelUpdateEvents(self):
  281.         try:
  282.             self.scheduler.remove()
  283.             self.scheduler = None
  284.         except:
  285.             pass
  286.  
  287.     # Subclasses should implement this
  288.     def update(self):
  289.         pass
  290.  
  291.     # Returns true iff this feed has been looked at
  292.     def getViewed(self):
  293.         ret = self.lastViewed != datetime.min
  294.         return ret
  295.  
  296.     # Returns the ID of the actual feed, never that of the UniversalFeed wrapper
  297.     def getFeedID(self):
  298.         return self.getID()
  299.  
  300.     def getID(self):
  301.         try:
  302.             return self.ufeed.getID()
  303.         except:
  304.             print "%s has no ufeed" % self
  305.  
  306.     # Returns true if x is a newly available item, otherwise returns false
  307.     def isAvailable(self, x):
  308.         return x.creationTime > self.lastViewed and (x.getState() == 'stopped' or x.getState() == 'downloading')
  309.  
  310.     # Returns true if x is an unwatched item, otherwise returns false
  311.     def isUnwatched(self, x):
  312.         state = x.getState()
  313.         return state == 'finished' or state == 'uploading'
  314.  
  315.     # Updates the state of unwatched and available items to meet
  316.     # Returns true iff endChange() is called
  317.     def updateUandA(self):
  318.         # Note: I'm not locking this with the assumption that we don't
  319.         #       care if these totals reflect an actual snapshot of the
  320.         #       database. If items change in the middle of this, oh well.
  321.         newU = 0
  322.         newA = 0
  323.         ret = False
  324.  
  325.         for item in self.items:
  326.             if self.isAvailable(item):
  327.                 newA += 1
  328.             if self.isUnwatched(item):
  329.                 newU += 1
  330.         self.ufeed.beginRead()
  331.         try:
  332.             if newU != self.unwatched or newA != self.available:
  333.                 self.ufeed.beginChange()
  334.                 try:
  335.                     ret = True
  336.                     self.unwatched = newU
  337.                     self.available = newA
  338.                 finally:
  339.                     self.ufeed.endChange()
  340.         finally:
  341.             self.ufeed.endRead()
  342.         return ret
  343.             
  344.     # Returns string with number of unwatched videos in feed
  345.     def numUnwatched(self):
  346.         return self.unwatched
  347.  
  348.     # Returns string with number of available videos in feed
  349.     def numAvailable(self):
  350.         return self.available
  351.  
  352.     # Returns true iff both unwatched and available numbers should be shown
  353.     def showBothUAndA(self):
  354.         return ((not self.isAutoDownloadable()) and
  355.                 self.unwatched > 0 and 
  356.                 self.available > 0)
  357.  
  358.     # Returns true iff unwatched should be shown and available shouldn't
  359.     def showOnlyU(self):
  360.         return ((self.unwatched > 0 and 
  361.                  self.available == 0) or 
  362.                 (self.isAutoDownloadable() and
  363.                  self.unwatched > 0))
  364.  
  365.     # Returns true iff available should be shown and unwatched shouldn't
  366.     def showOnlyA(self):
  367.         return ((not self.isAutoDownloadable()) and 
  368.                 self.unwatched == 0 and 
  369.                 self.available > 0)
  370.  
  371.     # Returns true iff neither unwatched nor available should be shown
  372.     def showNeitherUNorA(self):
  373.         return (self.unwatched == 0 and
  374.                 (self.isAutoDownloadable() or 
  375.                  self.available == 0))
  376.  
  377.     ##
  378.     # Sets the last time the feed was viewed to now
  379.     def markAsViewed(self):
  380.         # FIXME uncomment to make "new" state last 6 hours. See #655, #733
  381.         self.lastViewed = datetime.now() #- timedelta(hours=6)
  382.         self.updateUandA()
  383.  
  384.     ##
  385.     # Returns true iff the feed is loading. Only makes sense in the
  386.     # context of UniversalFeeds
  387.     def isLoading(self):
  388.         return False
  389.  
  390.     ##
  391.     # Returns true iff this feed has a library
  392.     def hasLibrary(self):
  393.         return False
  394.  
  395.     ##
  396.     # Downloads the next available item taking into account maxNew,
  397.     # fallbehind, and getEverything
  398.     def downloadNextAuto(self, dontUse = []):
  399.         self.ufeed.beginRead()
  400.         try:
  401.             next = None
  402.  
  403.             #The number of items downloading from this feed
  404.             dling = 0
  405.             #The number of items eligibile to download
  406.             eligibile = 0
  407.             #The number of unwatched, downloaded items
  408.             newitems = 0
  409.  
  410.             #Find the next item we should get
  411.             self.items.sort(sortFunc)
  412.             for item in self.items:
  413.                 if (item.getState() == "autopending") and not item in dontUse:
  414.                     eligibile += 1
  415.                     if next == None:
  416.                         next = item
  417.                     elif item.getPubDateParsed() > next.getPubDateParsed():
  418.                         next = item
  419.                 if item.getState() == "downloading":
  420.                     dling += 1
  421.                 if item.getState() == "finished" or item.getState() == "uploading" and not item.getSeen():
  422.                     newitems += 1
  423.  
  424.         finally:
  425.             self.ufeed.endRead()
  426.  
  427.         if self.maxNew >= 0 and newitems >= self.maxNew:
  428.             return False
  429.         elif self.fallBehind>=0 and eligibile > self.fallBehind:
  430.             dontUse.append(next)
  431.             return self.downloadNextAuto(dontUse)
  432.         elif next != None:
  433.             self.ufeed.beginRead()
  434.             try:
  435.                 self.startfrom = next.getPubDateParsed()
  436.             finally:
  437.                 self.ufeed.endRead()
  438.             next.download(autodl = True)
  439.             return True
  440.         else:
  441.             return False
  442.  
  443.     def downloadNextManual(self):
  444.         self.ufeed.beginRead()
  445.         next = None
  446.         self.items.sort(sortFunc)
  447.         for item in self.items:
  448.             if item.getState() == "manualpending":
  449.                 if next is None:
  450.                     next = item
  451.                 elif item.getPubDateParsed() < next.getPubDateParsed():
  452.                     next = item
  453.         if not next is None:
  454.             next.download(autodl = False)
  455.         self.ufeed.endRead()
  456.  
  457.     ##
  458.     # Returns marks expired items as expired
  459.     def expireItems(self):
  460.         expireTime = datetime.max - datetime.min
  461.         if self.expire == "feed":
  462.             expireTime = self.expireTime
  463.         elif self.expire == "system":
  464.             expireTime = timedelta(days=config.get(config.EXPIRE_AFTER_X_DAYS))
  465.             if expireTime <= timedelta(0):
  466.                 return
  467.         elif self.expire == "never":
  468.             return
  469.         for item in self.items:
  470.             local = item.getFilename() is not ""
  471.             expiring = datetime.now() - item.getDownloadedTime() > expireTime
  472.             stateOk = item.getState() in ('finished', 'stopped', 'watched')
  473.             keepIt = item.getKeep()
  474.             if local and expiring and stateOk and not keepIt:
  475.                 item.expire()
  476.  
  477.     ##
  478.     # Returns true iff feed should be visible
  479.     def isVisible(self):
  480.         self.ufeed.beginRead()
  481.         try:
  482.             ret = self.visible
  483.         finally:
  484.             self.ufeed.endRead()
  485.         return ret
  486.  
  487.     ##
  488.     # Switch the auto-downloadable state
  489.     def setAutoDownloadable(self, automatic):
  490.         self.ufeed.beginRead()
  491.         try:
  492.             self.autoDownloadable = (automatic == "1")
  493.             if self.autoDownloadable:
  494.                 self.startfrom = datetime.now()
  495.             else:
  496.                 self.startfrom = datetime.max
  497.         finally:
  498.             self.ufeed.endRead()
  499.  
  500.     ##
  501.     # Sets the 'getEverything' attribute, True or False
  502.     def setGetEverything(self, everything):
  503.         self.ufeed.beginRead()
  504.         try:
  505.             self.getEverything = everything
  506.         finally:
  507.             self.ufeed.endRead()
  508.  
  509.     ##
  510.     # Sets the expiration attributes. Valid types are 'system', 'feed' and 'never'
  511.     # Expiration time is in hour(s).
  512.     def setExpiration(self, type, time):
  513.         self.ufeed.beginRead()
  514.         try:
  515.             self.expire = type
  516.             self.expireTime = timedelta(hours=time)
  517.  
  518.             if self.expire == "never":
  519.                 for item in self.items:
  520.                     if item.getState() in ['finished','uploading','watched']:
  521.                         item.setKeep(True)
  522.         finally:
  523.             self.ufeed.endRead()
  524.  
  525.     ##
  526.     # Sets the maxNew attributes. -1 means unlimited.
  527.     def setMaxNew(self, maxNew):
  528.         self.ufeed.beginRead()
  529.         try:
  530.             self.maxNew = maxNew
  531.         finally:
  532.             self.ufeed.endRead()
  533.  
  534.     ##
  535.     # Return the 'system' expiration delay, in days (can be < 1.0)
  536.     def getDefaultExpiration(self):
  537.         return float(config.get(config.EXPIRE_AFTER_X_DAYS))
  538.  
  539.     ##
  540.     # Returns the 'system' expiration delay as a formatted string
  541.     def getFormattedDefaultExpiration(self):
  542.         expiration = self.getDefaultExpiration()
  543.         formattedExpiration = ''
  544.         if expiration < 0:
  545.             formattedExpiration = 'never'
  546.         elif expiration < 1.0:
  547.             formattedExpiration = '%d hours' % int(expiration * 24.0)
  548.         elif expiration == 1:
  549.             formattedExpiration = '%d day' % int(expiration)
  550.         elif expiration > 1 and expiration < 30:
  551.             formattedExpiration = '%d days' % int(expiration)
  552.         elif expiration >= 30:
  553.             formattedExpiration = '%d months' % int(expiration / 30)
  554.         return formattedExpiration
  555.  
  556.     ##
  557.     # Returns "feed," "system," or "never"
  558.     def getExpirationType(self):
  559.         self.ufeed.beginRead()
  560.         ret = self.expire
  561.         self.ufeed.endRead()
  562.         return ret
  563.  
  564.     ##
  565.     # Returns"unlimited" or the maximum number of items this feed can fall behind
  566.     def getMaxFallBehind(self):
  567.         self.ufeed.beginRead()
  568.         if self.fallBehind < 0:
  569.             ret = "unlimited"
  570.         else:
  571.             ret = self.fallBehind
  572.         self.ufeed.endRead()
  573.         return ret
  574.  
  575.     ##
  576.     # Returns "unlimited" or the maximum number of items this feed wants
  577.     def getMaxNew(self):
  578.         self.ufeed.beginRead()
  579.         if self.maxNew < 0:
  580.             ret = "unlimited"
  581.         else:
  582.             ret = self.maxNew
  583.         self.ufeed.endRead()
  584.         return ret
  585.  
  586.     ##
  587.     # Returns the total absolute expiration time in hours.
  588.     # WARNING: 'system' and 'never' expiration types return 0
  589.     def getExpirationTime(self):
  590.         delta = None
  591.         self.ufeed.beginRead()
  592.         try:
  593.             try:
  594.                 if self.expire == 'never' or (self.expire == 'system' and config.get(config.EXPIRE_AFTER_X_DAYS) <= 0):
  595.                     delta = timedelta()
  596.                 else:
  597.                     delta = self.expireTime
  598.             except:
  599.                 delta = timedelta()
  600.         finally:
  601.             self.ufeed.endRead()
  602.         return (delta.days * 24) + (delta.seconds / 3600)
  603.  
  604.     ##
  605.     # Returns the number of days until a video expires
  606.     def getExpireDays(self):
  607.         ret = 0
  608.         self.ufeed.beginRead()
  609.         try:
  610.             try:
  611.                 ret = self.expireTime.days
  612.             except:
  613.                 ret = timedelta(days=config.get(config.EXPIRE_AFTER_X_DAYS)).days
  614.         finally:
  615.             self.ufeed.endRead()
  616.         return ret
  617.  
  618.     ##
  619.     # Returns the number of hours until a video expires
  620.     def getExpireHours(self):
  621.         ret = 0
  622.         self.ufeed.beginRead()
  623.         try:
  624.             try:
  625.                 ret = int(self.expireTime.seconds/3600)
  626.             except:
  627.                 ret = int(timedelta(days=config.get(config.EXPIRE_AFTER_X_DAYS)).seconds/3600)
  628.         finally:
  629.             self.ufeed.endRead()
  630.         return ret
  631.         
  632.  
  633.     ##
  634.     # Returns true iff item is autodownloadable
  635.     def isAutoDownloadable(self):
  636.         self.ufeed.beginRead()
  637.         ret = self.autoDownloadable
  638.         self.ufeed.endRead()
  639.         return ret
  640.  
  641.     def autoDownloadStatus(self):
  642.         status = self.isAutoDownloadable()
  643.         if status:
  644.             return "ON"
  645.         else:
  646.             return "OFF"
  647.  
  648.     ##
  649.     # Returns the title of the feed
  650.     def getTitle(self):
  651.         try:
  652.             title = self.title
  653.             if whitespacePattern.match(title):
  654.                 title = self.url
  655.             return title
  656.         except:
  657.             return ""
  658.  
  659.     ##
  660.     # Returns the URL of the feed
  661.     def getURL(self):
  662.         try:
  663.             return self.url
  664.         except:
  665.             return ""
  666.  
  667.     ##
  668.     # Returns the description of the feed
  669.     def getDescription(self):
  670.         return "<span />"
  671.  
  672.     ##
  673.     # Returns a link to a webpage associated with the feed
  674.     def getLink(self):
  675.         return ""
  676.  
  677.     ##
  678.     # Returns the URL of the library associated with the feed
  679.     def getLibraryLink(self):
  680.         return ""
  681.  
  682.     ##
  683.     # Returns the URL of a thumbnail associated with the feed
  684.     def getThumbnail(self):
  685.         ret = self.thumbURL
  686.         if ret is None or not (ret.startswith('http:') or
  687.                                 ret.startswith('https:')):
  688.             ret = defaultFeedIconURL()
  689.         return ret
  690.  
  691.     ##
  692.     # Returns URL of license assocaited with the feed
  693.     def getLicense(self):
  694.         return ""
  695.  
  696.     ##
  697.     # Returns the number of new items with the feed
  698.     def getNewItems(self):
  699.         self.ufeed.beginRead()
  700.         count = 0
  701.         for item in self.items:
  702.             try:
  703.                 if item.getState() == 'finished' and not item.getSeen():
  704.                     count += 1
  705.             except:
  706.                 pass
  707.         self.ufeed.endRead()
  708.         return count
  709.  
  710. ##
  711. # This class is a magic class that can become any type of feed it wants
  712. #
  713. # It works by passing on attributes to the actual feed.
  714. class Feed(DDBObject):
  715.     def __init__(self,url, initial = None, initiallyAutoDownloadable = True):
  716.         self.origURL = url
  717.         self.errorState = False
  718.         self.initiallyAutoDownloadable = initiallyAutoDownloadable
  719.         if initial is None:
  720.             self.loading = True
  721.             self.actualFeed = FeedImpl(url,self)
  722.             DDBObject.__init__(self)
  723.             thread = Thread(target=lambda: self.generateFeed(True), \
  724.                             name="Feed.__init__ generate -- %s" % url)
  725.             thread.setDaemon(False)
  726.             thread.start()
  727.         else:
  728.             self.loading = False
  729.             self.actualFeed = initial
  730.  
  731.     # Returns javascript to mark the feed as viewed
  732.     # FIXME: Using setTimeout is a hack to get around JavaScript bugs
  733.     #        Without the timeout, the view is never completely updated
  734.     def getMarkViewedJS(self):
  735.         return ("function markViewed() {eventURL('action:markFeedViewed?url=%s');} setTimeout(markViewed, 5000);" % 
  736.                 urlencode(self.getURL()))
  737.  
  738.     # Returns the ID of this feed. Deprecated.
  739.     def getFeedID(self):
  740.         return self.getID()
  741.  
  742.     def getID(self):
  743.         return DDBObject.getID(self)
  744.  
  745.     def hasError(self):
  746.         ret = False
  747.         self.beginRead()
  748.         try:
  749.             ret = self.errorState
  750.         finally:
  751.             self.endRead()
  752.         return ret
  753.  
  754.     def getError(self):
  755.         return "Could not load feed"
  756.  
  757.     def update(self):
  758.         self.beginRead()
  759.         try:
  760.             if self.loading:
  761.                 return
  762.             elif self.errorState:
  763.                 self.loading = True
  764.                 self.errorState = False
  765.                 self.beginChange()
  766.                 self.endChange()
  767.                 thread = Thread(target=lambda: self.generateFeed(), \
  768.                                 name="Feed.update generate -- %s" % \
  769.                                 self.origURL)
  770.                 thread.setDaemon(False)
  771.                 thread.start()
  772.                 return
  773.         finally:
  774.             self.endRead()
  775.         self.actualFeed.update()
  776.  
  777.     def generateFeed(self, removeOnError=False):
  778.         temp =  _generateFeed(self.url,self,visible=True)
  779.         self.beginRead()
  780.         try:
  781.             self.loading = False
  782.             if temp is None:
  783.                 self.errorState = True
  784.             else:
  785.                 self.actualFeed = temp
  786.         finally:
  787.             self.endRead()
  788.  
  789.         if removeOnError and self.errorState:
  790.             self.remove()
  791.         else:
  792.             self.beginChange()
  793.             self.endChange()
  794.  
  795.     def getActualFeed(self):
  796.         return self.__dict__['actualFeed']
  797.  
  798.     def __getattr__(self,attr):
  799.         return getattr(self.getActualFeed(),attr)
  800.  
  801.     def remove(self):
  802.         self.beginChange()
  803.         self.cancelUpdateEvents()
  804.         try:
  805.             DDBObject.remove(self)
  806.             for item in self.items:
  807.                 if not item.getKeep():
  808.                     item.expire()
  809.                 item.remove()
  810.         finally:
  811.             self.endChange()
  812.         
  813.  
  814.     ##
  815.     # Called by pickle during serialization
  816.     def __getstate__(self):
  817.         temp = copy(self.__dict__)
  818.         #temp["itemlist"] = None
  819.         return (3,temp)
  820.  
  821.     ##
  822.     # Called by pickle during deserialization
  823.     def __setstate__(self,state):
  824.         (version, data) = state
  825.         if version == 0:
  826.             version += 1
  827.         if version == 1:
  828.             data['thumbURL'] = defaultFeedIconURL()
  829.             version += 1
  830.         if version == 2:
  831.             data['lastViewed'] = datetime.min
  832.             data['unwatched'] = 0
  833.             data['available'] = 0
  834.             version += 1
  835.         assert(version == 3)
  836.         data['updating'] = False
  837.         self.__dict__ = data
  838.         # This object is useless without a FeedImpl associated with it
  839.         if not data.has_key('actualFeed'):
  840.             self.__class__ = DropItLikeItsHot
  841.  
  842. # Dummy class to facilitate upgrade
  843. class YahooSearchFeedImpl:
  844.     def __setstate__(self,state):
  845.         self.__class__ = DropItLikeItsHot
  846.  
  847. class RSSFeedImpl(FeedImpl):
  848.     firstImageRE = re.compile('\<\s*img\s+[^>]*src\s*=\s*"(.*?)"[^>]*\>',re.I|re.M)
  849.     
  850.     def __init__(self,url,ufeed,title = None,initialHTML = None, etag = None, modified = None, visible=True):
  851.         FeedImpl.__init__(self,url,ufeed,title,visible=visible)
  852.         self.initialHTML = initialHTML
  853.         self.etag = etag
  854.         self.modified = modified
  855.         self.scheduleUpdateEvents(0)
  856.  
  857.     ##
  858.     # Returns the description of the feed
  859.     def getDescription(self):
  860.         self.ufeed.beginRead()
  861.         try:
  862.             ret = xhtmlify('<span>'+unescape(self.parsed.summary)+'</span>')
  863.         except:
  864.             ret = "<span />"
  865.         self.ufeed.endRead()
  866.         return ret
  867.  
  868.     ##
  869.     # Returns a link to a webpage associated with the feed
  870.     def getLink(self):
  871.         self.ufeed.beginRead()
  872.         try:
  873.             ret = self.parsed.link
  874.         except:
  875.             ret = ""
  876.         self.ufeed.endRead()
  877.         return ret
  878.  
  879.     ##
  880.     # Returns the URL of the library associated with the feed
  881.     def getLibraryLink(self):
  882.         self.ufeed.beginRead()
  883.         try:
  884.             ret = self.parsed.libraryLink
  885.         except:
  886.             ret = ""
  887.         self.ufeed.endRead()
  888.         return ret        
  889.  
  890.     def hasVideoFeed(self, enclosures):
  891.         hasOne = False
  892.         for enclosure in enclosures:
  893.             if ((enclosure.has_key('type') and
  894.                  (enclosure['type'].startswith('video/') or
  895.                   enclosure['type'].startswith('audio/') or
  896.                   enclosure['type'] == "application/x-bittorrent")) or
  897.                 (enclosure.has_key('url') and
  898.                  (enclosure['url'][-4:].lower() in ['.mov','.wmv','.mp4', '.m4v',
  899.                                                    '.mp3','.mpg','.avi'] or
  900.                   enclosure['url'][-8].lower() == '.torrent' or
  901.                   enclosure['url'][-5].lower() == '.mpeg'))):
  902.                 hasOne = True
  903.                 break
  904.         return hasOne
  905.  
  906.     ##
  907.     # Updates a feed
  908.     def update(self):
  909.         info = {}
  910.         self.ufeed.beginRead()
  911.         try:
  912.             if self.updating:
  913.                 return
  914.             else:
  915.                 self.updating = True
  916.         finally:
  917.             self.ufeed.endRead()
  918.         if not self.initialHTML is None:
  919.             html = self.initialHTML
  920.             self.initialHTML = None
  921.         else:
  922.             info = grabURL(self.url,etag=self.etag,modified=self.modified)
  923.             if info is None:
  924.                 self.ufeed.beginRead()
  925.                 try:
  926.                     self.updating = False
  927.                 finally:
  928.                     self.finishUpdate()
  929.                 return None
  930.             
  931.             html = info['file-handle'].read()
  932.             info['file-handle'].close()
  933.             if info.has_key('charset'):
  934.                 html = fixXMLHeader(html,info['charset'])
  935.             if info['status'] == 304:
  936.                 self.ufeed.beginRead()
  937.                 try:
  938.                     self.updating = False
  939.                 finally:
  940.                     self.finishUpdate()
  941.                 return
  942.             self.url = info['updated-url']
  943.         d = feedparser.parse(html)
  944.         self.parsed = d
  945.  
  946.         self.ufeed.beginRead()
  947.         try:
  948.             try:
  949.                 self.title = self.parsed["feed"]["title"]
  950.             except KeyError:
  951.                 try:
  952.                     self.title = self.parsed["channel"]["title"]
  953.                 except KeyError:
  954.                     pass
  955.             if (self.parsed.feed.has_key('image') and 
  956.                 self.parsed.feed.image.has_key('url')):
  957.                 self.thumbURL = self.parsed.feed.image.url
  958.             for entry in self.parsed.entries:
  959.                 entry = self.addScrapedThumbnail(entry)
  960.                 new = True
  961.                 for item in self.items:
  962.                     try:
  963.                         if item.getRSSID() == entry["id"]:
  964.                             item.update(entry)
  965.                             new = False
  966.                     except KeyError:
  967.                         # If the item changes at all, it results in a
  968.                         # new entry
  969.                         if (item.getRSSEntry() == entry):
  970.                             item.update(entry)
  971.                             new = False
  972.                 if (new and entry.has_key('enclosures') and
  973.                     self.hasVideoFeed(entry.enclosures)):
  974.                     self.items.append(Item(self.ufeed,entry))
  975.             try:
  976.                 updateFreq = self.parsed["feed"]["ttl"]
  977.             except KeyError:
  978.                 updateFreq = 0
  979.             self.setUpdateFrequency(updateFreq)
  980.             
  981.             self.updating = False
  982.         finally:
  983.             self.finishUpdate(info)
  984.  
  985.     def finishUpdate(self, info=None):
  986.         if info is not None:
  987.             if info.has_key('etag'):
  988.                 self.etag = info['etag']
  989.             if info.has_key('last-modified'):
  990.                 self.modified = info['last-modified']
  991.         self.ufeed.endRead() #FIXMENOW This is sloow...
  992.         if not self.updateUandA():
  993.             self.ufeed.beginChange()
  994.             self.ufeed.endChange()
  995.  
  996.     def addScrapedThumbnail(self,entry):
  997.         if (entry.has_key('enclosures') and len(entry['enclosures'])>0 and
  998.             entry.has_key('description') and 
  999.             not entry['enclosures'][0].has_key('thumbnail')):
  1000.                 desc = RSSFeedImpl.firstImageRE.search(unescape(entry['description']))
  1001.                 if not desc is None:
  1002.                     entry['enclosures'][0]['thumbnail'] = FeedParserDict({'url': desc.expand("\\1")})
  1003.         return entry
  1004.  
  1005.     ##
  1006.     # Returns the URL of the license associated with the feed
  1007.     def getLicense(self):
  1008.         try:
  1009.             ret = self.parsed.license
  1010.         except:
  1011.             ret = ""
  1012.         return ret
  1013.  
  1014.     ##
  1015.     # Called by pickle during serialization
  1016.     def __getstate__(self):
  1017.         temp = copy(self.__dict__)
  1018.         temp["scheduler"] = None
  1019.         if temp.has_key('parsed') and 'bozo_exception' in temp['parsed']:
  1020.             # This can end up pointing into the XML parser, leading to
  1021.             # a pickling failure.
  1022.             del temp['parsed']['bozo_exception']
  1023.         #temp["itemlist"] = None
  1024.         return (0,temp)
  1025.  
  1026.     ##
  1027.     # Called by pickle during deserialization
  1028.     def __setstate__(self,state):
  1029.         (version, data) = state
  1030.         assert(version == 0)
  1031.         data['updating'] = False
  1032.         self.__dict__ = data
  1033.         #self.itemlist = defaultDatabase.filter(lambda x:isinstance(x,Item) and x.feed is self)
  1034.         #FIXME: the update dies if all of the items aren't restored, so we 
  1035.         # wait a little while before we start the update
  1036.         self.scheduleUpdateEvents(0.1)
  1037.  
  1038.  
  1039. ##
  1040. # A DTV Collection of items -- similar to a playlist
  1041. class Collection(FeedImpl):
  1042.     def __init__(self,ufeed,title = None):
  1043.         FeedImpl.__init__(self,ufeed,url = "dtv:collection",title = title,visible = False)
  1044.  
  1045.     ##
  1046.     # Adds an item to the collection
  1047.     def addItem(self,item):
  1048.         if isinstance(item,Item):
  1049.             self.ufeed.beginRead()
  1050.             try:
  1051.                 self.removeItem(item)
  1052.                 self.items.append(item)
  1053.             finally:
  1054.                 self.ufeed.endRead()
  1055.             return True
  1056.         else:
  1057.             return False
  1058.  
  1059.     ##
  1060.     # Moves an item to another spot in the collection
  1061.     def moveItem(self,item,pos):
  1062.         self.ufeed.beginRead()
  1063.         try:
  1064.             self.removeItem(item)
  1065.             if pos < len(self.items):
  1066.                 self.items[pos:pos] = [item]
  1067.             else:
  1068.                 self.items.append(item)
  1069.         finally:
  1070.             self.ufeed.endRead()
  1071.  
  1072.     ##
  1073.     # Removes an item from the collection
  1074.     def removeItem(self,item):
  1075.         self.ufeed.beginRead()
  1076.         try:
  1077.             for x in range(0,len(self.items)):
  1078.                 if self.items[x] == item:
  1079.                     self.items[x:x+1] = []
  1080.                     break
  1081.         finally:
  1082.             self.ufeed.endRead()
  1083.         return True
  1084.  
  1085. ##
  1086. # A feed based on un unformatted HTML or pre-enclosure RSS
  1087. class ScraperFeedImpl(FeedImpl):
  1088.     #FIXME: change this to a higher number once we optimize a bit
  1089.     maxThreads = 1
  1090.  
  1091.     def __init__(self,url,ufeed, title = None, visible = True, initialHTML = None,etag=None,modified = None,charset = None):
  1092.         FeedImpl.__init__(self,url,ufeed,title,visible)
  1093.         self.initialHTML = initialHTML
  1094.         self.initialCharset = charset
  1095.         self.linkHistory = {}
  1096.         self.linkHistory[url] = {}
  1097.         self.tempHistory = {}
  1098.         if not etag is None:
  1099.             self.linkHistory[url]['etag'] = etag
  1100.         if not modified is None:
  1101.             self.linkHistory[url]['modified'] = modified
  1102.         self.semaphore = Semaphore(ScraperFeedImpl.maxThreads)
  1103.         self.scheduleUpdateEvents(0)
  1104.         self.setUpdateFrequency(360)
  1105.  
  1106.     def getMimeType(self,link):
  1107.         info = grabURL(link,"HEAD")
  1108.         if info is None:
  1109.             return ''
  1110.         else:
  1111.             return info['content-type']
  1112.  
  1113.     ##
  1114.     # This puts all of the caching information in tempHistory into the
  1115.     # linkHistory. This should be called at the end of an updated so that
  1116.     # the next time we update we don't unnecessarily follow old links
  1117.     def saveCacheHistory(self):
  1118.         self.ufeed.beginRead()
  1119.         try:
  1120.             for url in self.tempHistory.keys():
  1121.                 self.linkHistory[url] = self.tempHistory[url]
  1122.             self.tempHistory = {}
  1123.         finally:
  1124.             self.ufeed.endRead()
  1125.     ##
  1126.     # returns a tuple containing the text of the URL, the url (in case
  1127.     # of a permanent redirect), a redirected URL (in case of
  1128.     # temporary redirect)m and the download status
  1129.     def getHTML(self, url, useActualHistory = True):
  1130.         etag = None
  1131.         modified = None
  1132.         if self.linkHistory.has_key(url):
  1133.             if self.linkHistory[url].has_key('etag'):
  1134.                 etag = self.linkHistory[url]['etag']
  1135.             if self.linkHistory[url].has_key('modified'):
  1136.                 modified = self.linkHistory[url]['modified']
  1137.         info = grabURL(url, etag=etag, modified=modified)
  1138.         if info is None:
  1139.             return (None, url, url,404, None)
  1140.         else:
  1141.             if not self.tempHistory.has_key(info['updated-url']):
  1142.                 self.tempHistory[info['updated-url']] = {}
  1143.             if info.has_key('etag'):
  1144.                 self.tempHistory[info['updated-url']]['etag'] = info['etag']
  1145.             if info.has_key('last-modified'):
  1146.                 self.tempHistory[info['updated-url']]['modified'] = info['last-modified']
  1147.  
  1148.             html = info['file-handle'].read()
  1149.             #print "Scraper got HTML of length "+str(len(html))
  1150.             info['file-handle'].close()
  1151.             #print "Closed"
  1152.             if info.has_key('charset'):
  1153.                 return (html, info['updated-url'],info['redirected-url'],info['status'],info['charset'])
  1154.             else:
  1155.                 return (html, info['updated-url'],info['redirected-url'],info['status'],None)
  1156.  
  1157.     def addVideoItem(self,link,dict,linkNumber):
  1158.         link = link.strip()
  1159.         if dict.has_key('title'):
  1160.             title = dict['title']
  1161.         else:
  1162.             title = link
  1163.         for item in self.items:
  1164.             if item.getURL() == link:
  1165.                 return
  1166.         if dict.has_key('thumbnail') > 0:
  1167.             i=Item(self.ufeed, FeedParserDict({'title':title,'enclosures':[FeedParserDict({'url':link,'thumbnail':FeedParserDict({'url':dict['thumbnail']})})]}),linkNumber = linkNumber)
  1168.         else:
  1169.             i=Item(self.ufeed, FeedParserDict({'title':title,'enclosures':[FeedParserDict({'url':link})]}),linkNumber = linkNumber)
  1170.         self.items.append(i)
  1171.         if not self.updateUandA():
  1172.             self.ufeed.beginChange()
  1173.             self.ufeed.endChange()
  1174.  
  1175.     def makeProcessLinkFunc(self,subLinks,depth,linkNumber):
  1176.         return lambda: self.processLinksThenFreeSem(subLinks,depth,linkNumber)
  1177.  
  1178.     def processLinksThenFreeSem(self,subLinks,depth,linkNumber):
  1179.         try:
  1180.             self.processLinks(subLinks, depth,linkNumber)
  1181.         finally:
  1182.             #print "Releasing semaphore"
  1183.             self.semaphore.release()
  1184.  
  1185.     #FIXME: compound names for titles at each depth??
  1186.     def processLinks(self,links, depth = 0,linkNumber = 0):
  1187.         maxDepth = 2
  1188.         urls = links[0]
  1189.         links = links[1]
  1190.         if depth<maxDepth:
  1191.             for link in urls:
  1192.                 if depth == 0:
  1193.                     linkNumber += 1
  1194.                 #print "Processing %s (%d)" % (link,linkNumber)
  1195.  
  1196.                 # FIXME: Using file extensions totally breaks the
  1197.                 # standard and won't work with Broadcast Machine or
  1198.                 # Blog Torrent. However, it's also a hell of a lot
  1199.                 # faster than checking the mime type for every single
  1200.                 # file, so for now, we're being bad boys. Uncomment
  1201.                 # the elif to make this use mime types for HTTP GET URLs
  1202.  
  1203.                 if ((link[-4:].lower() in 
  1204.                      ['.mov','.wmv','.mp4','.m4v','.mp3','.mpg','.avi']) or
  1205.                     (link[-5:].lower() in ['.mpeg'])):
  1206.                     mimetype = 'video/unknown'
  1207.                 elif link[-8:].lower() == '.torrent':
  1208.                     mimetype = "application/x-bittorrent"
  1209.                 #elif link.find('?') > 0 and link.lower().find('.htm') == -1:
  1210.                 #    mimetype = self.getMimeType(link)
  1211.                 #    #print " mimetype is "+mimetype
  1212.                 else:
  1213.                     mimetype = 'text/html'
  1214.                 if mimetype != None:
  1215.                     #This is text of some sort: HTML, XML, etc.
  1216.                     if ((mimetype.startswith('text/html') or
  1217.                          mimetype.startswith('application/xhtml+xml') or 
  1218.                          mimetype.startswith('text/xml')  or
  1219.                          mimetype.startswith('application/xml') or
  1220.                          mimetype.startswith('application/rss+xml') or
  1221.                          mimetype.startswith('application/podcast+xml') or
  1222.                          mimetype.startswith('application/atom+xml') or
  1223.                          mimetype.startswith('application/rdf+xml') ) and
  1224.                         depth < maxDepth -1):
  1225.                         (html, url, redirURL,status,charset) = self.getHTML(link)
  1226.                         if status == 304: #It's cached
  1227.                             pass
  1228.                         elif not html is None:
  1229.                             subLinks = self.scrapeLinks(html, redirURL,charset=charset)
  1230.                             if depth == 0:
  1231.                                 self.semaphore.acquire()
  1232.                                 #print "Acquiring semaphore"
  1233.                                 thread = Thread(target = self.makeProcessLinkFunc(subLinks,depth+1,linkNumber), \
  1234.                                                 name = "scraper processLinks -- %s" % self.url)
  1235.                                 thread.setDaemon(False)
  1236.                                 thread.start()
  1237.                             else:
  1238.                                 self.processLinks(subLinks,depth+1,linkNumber)
  1239.                         else:
  1240.                             pass
  1241.                             #print link+" seems to be bogus..."
  1242.                     #This is a video
  1243.                     elif (mimetype.startswith('video/') or 
  1244.                           mimetype.startswith('audeo/') or
  1245.                           mimetype == "application/x-bittorrent"):
  1246.                         self.addVideoItem(link, links[link],linkNumber)
  1247.  
  1248.     #FIXME: go through and add error handling
  1249.     def update(self):
  1250.         self.ufeed.beginRead()
  1251.         try:
  1252.             if self.updating:
  1253.                 return
  1254.             else:
  1255.                 self.updating = True
  1256.         finally:
  1257.             self.ufeed.endRead()
  1258.         if not self.initialHTML is None:
  1259.             html = self.initialHTML
  1260.             self.initialHTML = None
  1261.             redirURL=self.url
  1262.             status = 200
  1263.             charset = self.initialCharset
  1264.             self.initialCharset = None
  1265.         else:
  1266.             (html,url, redirURL, status,charset) = self.getHTML(self.url)
  1267.         if not status == 304:
  1268.             if not html is None:
  1269.                 links = self.scrapeLinks(html, redirURL, setTitle=True,charset=charset)
  1270.                 self.processLinks(links)
  1271.             #Download the HTML associated with each page
  1272.         self.ufeed.beginRead()
  1273.         try:
  1274.             self.saveCacheHistory()
  1275.             self.updating = False
  1276.         finally:
  1277.             self.ufeed.endRead()
  1278.  
  1279.     def scrapeLinks(self,html,baseurl,setTitle = False,charset = None):
  1280.         try:
  1281.             if not charset is None:
  1282.                 xmldata = fixXMLHeader(html,charset)
  1283.                 html = fixHTMLHeader(html,charset)
  1284.             else:
  1285.                 xmldata = html
  1286.             parser = xml.sax.make_parser()
  1287.             parser.setFeature(xml.sax.handler.feature_namespaces, 1)
  1288.             if not charset is None:
  1289.                 handler = RSSLinkGrabber(baseurl,charset)
  1290.             else:
  1291.                 handler = RSSLinkGrabber(baseurl)
  1292.             parser.setContentHandler(handler)
  1293.             try:
  1294.                 parser.parse(StringIO(xmldata))
  1295.             except IOError, e:
  1296.                 pass
  1297.             links = handler.links
  1298.             linkDict = {}
  1299.             for link in links:
  1300.                 if link[0].startswith('http://') or link[0].startswith('https://'):
  1301.                     if not linkDict.has_key(toUTF8Bytes(link[0],charset)):
  1302.                         linkDict[toUTF8Bytes(link[0])] = {}
  1303.                     if not link[1] is None:
  1304.                         linkDict[toUTF8Bytes(link[0])]['title'] = toUTF8Bytes(link[1],charset).strip()
  1305.                     if not link[2] is None:
  1306.                         linkDict[toUTF8Bytes(link[0])]['thumbnail'] = toUTF8Bytes(link[2],charset)
  1307.             if setTitle and not handler.title is None:
  1308.                 self.ufeed.beginChange()
  1309.                 try:
  1310.                     self.title = toUTF8Bytes(handler.title)
  1311.                 finally:
  1312.                     self.ufeed.endChange()
  1313.             return ([x[0] for x in links if x[0].startswith('http://') or x[0].startswith('https://')], linkDict)
  1314.         except (xml.sax.SAXException, IOError):
  1315.             (links, linkDict) = self.scrapeHTMLLinks(html,baseurl,setTitle=setTitle, charset=charset)
  1316.             return (links, linkDict)
  1317.  
  1318.     ##
  1319.     # Given a string containing an HTML file, return a dictionary of
  1320.     # links to titles and thumbnails
  1321.     def scrapeHTMLLinks(self,html, baseurl,setTitle=False, charset = None):
  1322.         #print "Scraping "+baseurl+" as HTML"
  1323.         lg = HTMLLinkGrabber()
  1324.         links = lg.getLinks(html, baseurl)
  1325.         if setTitle and not lg.title is None:
  1326.             self.ufeed.beginChange()
  1327.             try:
  1328.                 self.title = toUTF8Bytes(lg.title)
  1329.             finally:
  1330.                 self.ufeed.endChange()
  1331.             
  1332.         linkDict = {}
  1333.         for link in links:
  1334.             if link[0].startswith('http://') or link[0].startswith('https://'):
  1335.                 if not linkDict.has_key(toUTF8Bytes(link[0],charset)):
  1336.                     linkDict[toUTF8Bytes(link[0])] = {}
  1337.                 if not link[1] is None:
  1338.                     linkDict[toUTF8Bytes(link[0])]['title'] = toUTF8Bytes(link[1],charset).strip()
  1339.                 if not link[2] is None:
  1340.                     linkDict[toUTF8Bytes(link[0])]['thumbnail'] = toUTF8Bytes(link[2],charset)
  1341.         return ([x[0] for x in links if x[0].startswith('http://') or x[0].startswith('https://')],linkDict)
  1342.         
  1343.     ##
  1344.     # Called by pickle during serialization
  1345.     def __getstate__(self):
  1346.         temp = copy(self.__dict__)
  1347.         temp['semaphore'] = None
  1348.         temp["scheduler"] = None
  1349.         #temp["itemlist"] = None
  1350.         return (0,temp)
  1351.  
  1352.     ##
  1353.     # Called by pickle during deserialization
  1354.     def __setstate__(self,state):
  1355.         (version, data) = state
  1356.         assert(version == 0)
  1357.         data['updating'] = False
  1358.         data['tempHistory'] = {}
  1359.         self.__dict__ = data
  1360.         #self.itemlist = defaultDatabase.filter(lambda x:isinstance(x,Item) and x.feed is self)
  1361.  
  1362.         #FIXME: the update dies if all of the items aren't restored, so we 
  1363.         # wait a little while before we start the update
  1364.         self.scheduleUpdateEvents(.1)
  1365.         self.semaphore = Semaphore(ScraperFeedImpl.maxThreads)
  1366.  
  1367. ##
  1368. # A feed of all of the Movies we find in the movie folder that don't
  1369. # belong to a "real" feed
  1370. #
  1371. # FIXME: How do we trigger updates on this feed?
  1372. class DirectoryFeedImpl(FeedImpl):
  1373.  
  1374.     def __init__(self,ufeed):
  1375.         FeedImpl.__init__(self,url = "dtv:directoryfeed",ufeed=ufeed,title = "Feedless Videos",visible = False)
  1376.  
  1377.         self.setUpdateFrequency(5)
  1378.         self.scheduleUpdateEvents(0)
  1379.  
  1380.     ##
  1381.     # Directory Items shouldn't automatically expire
  1382.     def expireItems(self):
  1383.         pass
  1384.  
  1385.     def setUpdateFrequency(self, frequency):
  1386.         newFreq = frequency*60
  1387.         if newFreq != self.updateFreq:
  1388.                 self.updateFreq = newFreq
  1389.                 self.scheduleUpdateEvents(-1)
  1390.  
  1391.     ##
  1392.     # Returns a list of all of the files in a given directory
  1393.     def getFileList(self,dir):
  1394.         allthefiles = []
  1395.         for root, dirs, files in os.walk(dir,topdown=True):
  1396.             if root == dir and 'Incomplete Downloads' in dirs:
  1397.                 dirs.remove('Incomplete Downloads')
  1398.             toRemove = []
  1399.             for curdir in dirs:
  1400.                 if curdir[0] == '.':
  1401.                     toRemove.append(curdir)
  1402.             for curdir in toRemove:
  1403.                 dirs.remove(curdir)
  1404.             toRemove = []
  1405.             for curfile in files:
  1406.                 if curfile[0] == '.':
  1407.                     toRemove.append(curfile)
  1408.             for curfile in toRemove:
  1409.                 files.remove(curfile)
  1410.             
  1411.             allthefiles[:0] = map(lambda x:os.path.normcase(os.path.join(root,x)),files)
  1412.         return allthefiles
  1413.  
  1414.     def update(self):
  1415.         self.ufeed.beginRead()
  1416.         try:
  1417.             if self.updating:
  1418.                 return
  1419.             else:
  1420.                 self.updating = True
  1421.         finally:
  1422.             self.ufeed.endRead()
  1423.         knownFiles = []
  1424.         #Files on the filesystem
  1425.         existingFiles = self.getFileList(config.get(config.MOVIES_DIRECTORY))
  1426.         #Files known about by real feeds
  1427.         for item in app.globalViewList['items']:
  1428.             if not item.feed is self.ufeed:
  1429.                 knownFiles[:0] = item.getFilenames()
  1430.         knownFiles = map(os.path.normcase,knownFiles)
  1431.  
  1432.         #Remove items that are in feeds, but we have in our list
  1433.         for x in range(0,len(self.items)):
  1434.             try:
  1435.                 while (self.items[x].getFilename() in knownFiles) or (not self.items[x].getFilename() in existingFiles):
  1436.                     self.items[x].remove()
  1437.                     self.items[x:x+1] = []
  1438.             except IndexError:
  1439.                 pass
  1440.  
  1441.         #Files on the filesystem that we known about
  1442.         myFiles = map(lambda x:x.getFilename(),self.items)
  1443.  
  1444.         #Adds any files we don't know about
  1445.         for file in existingFiles:
  1446.             if not file in knownFiles and not file in myFiles:
  1447.                 self.items.append(FileItem(self.ufeed,file))
  1448.         self.updating = False
  1449.  
  1450.     ##
  1451.     # Called by pickle during serialization
  1452.     def __getstate__(self):
  1453.         temp = copy(self.__dict__)
  1454.         temp["scheduler"] = None
  1455.         return (0,temp)
  1456.     def __setstate__(self,state):
  1457.         (version, data) = state
  1458.         assert(version == 0)
  1459.         data['updating'] = False
  1460.         self.__dict__ = data
  1461.  
  1462.         #FIXME: the update dies if all of the items aren't restored, so we 
  1463.         # wait a little while before we start the update
  1464.         self.scheduleUpdateEvents(.1)
  1465.  
  1466.  
  1467. ##
  1468. # Search and Search Results feeds
  1469.  
  1470. class SearchFeedImpl (RSSFeedImpl):
  1471.     
  1472.     def __init__(self, ufeed):
  1473.         RSSFeedImpl.__init__(self, url='', ufeed=ufeed, title='dtv:search', visible=False)
  1474.         self.setUpdateFrequency(-1)
  1475.         self.setAutoDownloadable(False)
  1476.         self.searching = False
  1477.         self.lastEngine = 'yahoo'
  1478.         self.lastQuery = ''
  1479.  
  1480.     def getStatus(self):
  1481.         status = 'idle-empty'
  1482.         if self.searching:
  1483.             status =  'searching'
  1484.         elif len(self.items) > 0:
  1485.             status =  'idle-with-results'
  1486.         return status
  1487.  
  1488.     def reset(self, url='', searchState=False):
  1489.         self.ufeed.beginChange()
  1490.         try:
  1491.             for item in self.items:
  1492.                 item.remove()
  1493.             self.items = []
  1494.             self.url = url
  1495.             self.searching = searchState
  1496.         finally:
  1497.             self.ufeed.endChange()
  1498.     
  1499.     def preserveDownloads(self, downloadsFeed):
  1500.         self.ufeed.beginRead()
  1501.         try:
  1502.             allItems = [] + self.items
  1503.             for item in allItems:
  1504.                 if item.getState() != 'stopped':
  1505.                     downloadsFeed.addItem(item)
  1506.         finally:
  1507.             self.ufeed.endRead()
  1508.         
  1509.     def lookup(self, engine, query):
  1510.         url = self.getRequestURL(engine, query)
  1511.         self.reset(url, True)
  1512.         self.lastQuery = query
  1513.         thread = Thread(target=self.update, \
  1514.                         name = "%s search -- %s" % (engine, query))
  1515.         thread.setDaemon(False)
  1516.         thread.start()
  1517.  
  1518.     def getRequestURL(self, engine, query, filterAdultContents=True, limit=50):
  1519.         if query == "LET'S TEST DTV'S CRASH REPORTER TODAY":
  1520.             someVariable = intentionallyUndefinedVariableToTestCrashReporter
  1521.  
  1522.         if engine == 'yahoo':
  1523.             url =  "http://api.search.yahoo.com/VideoSearchService/rss/videoSearch.xml"
  1524.             url += "?appid=dtv_search"
  1525.             url += "&adult_ok=%d" % int(not filterAdultContents)
  1526.             url += "&results=%d" % limit
  1527.             url += "&format=any"
  1528.             url += "&query=%s" % urlencode(query)
  1529.         elif engine == 'blogdigger':
  1530.             url =  "http://blogdigger.com/media/rss.jsp"
  1531.             url += "?q=%s" % urlencode(query)
  1532.             url += "&media=video"
  1533.             url += "&media=torrent"
  1534.             url += "&sortby=date"
  1535.         return url
  1536.  
  1537.     def update(self):
  1538.         if self.url is not None and self.url != '':
  1539.             RSSFeedImpl.update(self)
  1540.  
  1541.     def finishUpdate(self, info=None):
  1542.         self.searching = False
  1543.         RSSFeedImpl.finishUpdate(self, info)
  1544.                     
  1545.  
  1546. class SearchDownloadsFeedImpl (FeedImpl):
  1547.  
  1548.     def __init__(self, ufeed):
  1549.         FeedImpl.__init__(self, url='dtv:searchDownloads', ufeed=ufeed, title=None, visible=False)
  1550.         self.setUpdateFrequency(-1)
  1551.         
  1552.     def addItem(self, item):
  1553.         self.ufeed.beginRead()
  1554.         try:
  1555.             if not item in self.items:
  1556.                 item.beginRead()
  1557.                 try:
  1558.                     item.feed.items.remove(item)
  1559.                     item.feed = self.ufeed
  1560.                 finally:
  1561.                     item.endRead()
  1562.                 self.items.append(item)
  1563.         finally:
  1564.             self.ufeed.endRead()
  1565.  
  1566.  
  1567. ##
  1568. # Parse HTML document and grab all of the links and their title
  1569. # FIXME: Grab link title from ALT tags in images
  1570. # FIXME: Grab document title from TITLE tags
  1571. class HTMLLinkGrabber(HTMLParser):
  1572.     linkPattern = re.compile("^.*?<(a|embed)\s.*?(href|src)\s*=\s*\"(.*?)\".*?>(.*?)</a>(.*)$", re.S)
  1573.     imgPattern = re.compile(".*<img\s.*?src\s*=\s*\"(.*?)\".*?>", re.S)
  1574.     tagPattern = re.compile("<.*?>")
  1575.     def getLinks(self,data, baseurl):
  1576.         self.links = []
  1577.         self.lastLink = None
  1578.         self.inLink = False
  1579.         self.inObject = False
  1580.         self.baseurl = baseurl
  1581.         self.inTitle = False
  1582.         self.title = None
  1583.         self.thumbnailUrl = None
  1584.  
  1585.         match = HTMLLinkGrabber.linkPattern.match(data)
  1586.         while match:
  1587.             link = urljoin(baseurl, match.group(3))
  1588.             desc = match.group(4)
  1589.             imgMatch = HTMLLinkGrabber.imgPattern.match(desc)
  1590.             if imgMatch:
  1591.                 thumb = urljoin(baseurl, imgMatch.group(1))
  1592.             else:
  1593.                 thumb = None
  1594.             desc =  HTMLLinkGrabber.tagPattern.sub(' ',desc)
  1595.             self.links.append( (link, desc, thumb))
  1596.             match = HTMLLinkGrabber.linkPattern.match(match.group(5))
  1597.         return self.links
  1598.  
  1599. class RSSLinkGrabber(xml.sax.handler.ContentHandler):
  1600.     def __init__(self,baseurl,charset=None):
  1601.         self.baseurl = baseurl
  1602.         self.charset = charset
  1603.     def startDocument(self):
  1604.         #print "Got start document"
  1605.         self.enclosureCount = 0
  1606.         self.itemCount = 0
  1607.         self.links = []
  1608.         self.inLink = False
  1609.         self.inDescription = False
  1610.         self.inTitle = False
  1611.         self.inItem = False
  1612.         self.descHTML = ''
  1613.         self.theLink = ''
  1614.         self.title = None
  1615.         self.firstTag = True
  1616.  
  1617.     def startElementNS(self, name, qname, attrs):
  1618.         (uri, tag) = name
  1619.         if self.firstTag:
  1620.             self.firstTag = False
  1621.             if tag != 'rss':
  1622.                 raise xml.sax.SAXNotRecognizedException, "Not an RSS file"
  1623.         if tag.lower() == 'enclosure' or tag.lower() == 'content':
  1624.             self.enclosureCount += 1
  1625.         elif tag.lower() == 'link':
  1626.             self.inLink = True
  1627.             self.theLink = ''
  1628.         elif tag.lower() == 'description':
  1629.             self.inDescription = True
  1630.             self.descHTML = ''
  1631.         elif tag.lower() == 'item':
  1632.             self.itemCount += 1
  1633.             self.inItem = True
  1634.         elif tag.lower() == 'title' and not self.inItem:
  1635.             self.inTitle = True
  1636.     def endElementNS(self, name, qname):
  1637.         (uri, tag) = name
  1638.         if tag.lower() == 'description':
  1639.             lg = HTMLLinkGrabber()
  1640.             try:
  1641.                 html = xhtmlify(unescape(self.descHTML),addTopTags=True)
  1642.                 if not self.charset is None:
  1643.                     html = fixHTMLHeader(html,self.charset)
  1644.                 self.links[:0] = lg.getLinks(html,self.baseurl)
  1645.             except HTMLParseError: # Don't bother with bad HTML
  1646.                 print "DTV: bad HTML in %s" % self.baseurl
  1647.             self.inDescription = False
  1648.         elif tag.lower() == 'link':
  1649.             self.links.append((self.theLink,None,None))
  1650.             self.inLink = False
  1651.         elif tag.lower() == 'item':
  1652.             self.inItem == False
  1653.         elif tag.lower() == 'title' and not self.inItem:
  1654.             self.inTitle = False
  1655.  
  1656.     def characters(self, data):
  1657.         if self.inDescription:
  1658.             self.descHTML += data
  1659.         elif self.inLink:
  1660.             self.theLink += data
  1661.         elif self.inTitle:
  1662.             if self.title is None:
  1663.                 self.title = data
  1664.             else:
  1665.                 self.title += data
  1666.  
  1667. # Grabs the feed link from the given webpage
  1668. class HTMLFeedURLParser(HTMLParser):
  1669.     def getLink(self,baseurl,data):
  1670.         self.baseurl = baseurl
  1671.         self.link = None
  1672.         try:
  1673.             self.feed(data)
  1674.         except HTMLParseError:
  1675.             print "DTV: error parsing "+str(baseurl)
  1676.         try:
  1677.             self.close()
  1678.         except HTMLParseError:
  1679.             print "DTV: error closing "+str(baseurl)
  1680.         return self.link
  1681.  
  1682.     def handle_starttag(self, tag, attrs):
  1683.         attrdict = {}
  1684.         for (key, value) in attrs:
  1685.             attrdict[key.lower()] = value
  1686.         if (tag.lower() == 'link' and attrdict.has_key('rel') and 
  1687.             attrdict.has_key('type') and attrdict.has_key('href') and
  1688.             attrdict['rel'].lower() == 'alternate' and 
  1689.             attrdict['type'].lower() in ['application/rss+xml',
  1690.                                          'application/podcast+xml',
  1691.                                          'application/rdf+xml',
  1692.                                          'application/atom+xml',
  1693.                                          'text/xml',
  1694.                                          'application/xml']):
  1695.             self.link = urljoin(self.baseurl,attrdict['href'])
  1696.  
  1697. class UniversalFeed:
  1698.     def __setstate__(self, state):
  1699.         (version, data) = state
  1700.         if version == 0:
  1701.             data['errorState'] = False
  1702.             version += 1
  1703.         if version == 1:
  1704.             version += 1
  1705.         assert(version == 2)
  1706.         self.__dict__ = data
  1707.         self.__class__ = Feed
  1708.         for key, val in Feed.__dict__.iteritems():
  1709.             if isfunction(val):
  1710.                 instancemethod(val, self,Feed)
  1711.         self.actualFeed.ufeed = self
  1712.         # UniversalFeeds should never contain Feeds. If they do,
  1713.         # something is wrong.
  1714.         if isinstance(self.actualFeed, Feed):
  1715.             self.__class__ = DropItLikeItsHot
  1716.         else:
  1717.             # FIXME: this assumes that the feed object is decoded before
  1718.             # it's items
  1719.             self.actualFeed.ufeed = self
  1720.                 
  1721. class ScraperFeed(ScraperFeedImpl):
  1722.     ##
  1723.     # Called by pickle during deserialization
  1724.     def __setstate__(self,state):
  1725.         (version, data) = state
  1726.         if version == 0:
  1727.             version += 1
  1728.         if version == 1:
  1729.             data['thumbURL'] = defaultFeedIconURL()
  1730.             version += 1
  1731.         if version == 2:
  1732.             data['lastViewed'] = datetime.min
  1733.             data['unwatched'] = 0
  1734.             data['available'] = 0
  1735.  
  1736.             version += 1
  1737.         assert(version == 3)
  1738.         data['updating'] = False
  1739.         data['tempHistory'] = {}
  1740.         data['visible'] = True
  1741.         self.__dict__ = data
  1742.         self.__class__ = ScraperFeedImpl
  1743.  
  1744. class DirectoryFeed(DirectoryFeedImpl):
  1745.     ##
  1746.     # Called by pickle during deserialization
  1747.     def __setstate__(self,state):
  1748.         (version, data) = state
  1749.         if version == 0:
  1750.             data['thumbURL'] = defaultFeedIconURL()
  1751.             version += 1
  1752.         if version == 1:
  1753.             data['lastViewed'] = datetime.min
  1754.             data['unwatched'] = 0
  1755.             data['available'] = 0
  1756.             version += 1
  1757.         assert(version == 2)
  1758.         data['updating'] = False
  1759.         self.__dict__ = data
  1760.         self.__class__ = DirectoryFeedImpl
  1761.  
  1762. class RSSFeed(RSSFeedImpl):
  1763.     ##
  1764.     # Called by pickle during deserialization
  1765.     def __setstate__(self,state):
  1766.         (version, data) = state
  1767.         if version == 0:
  1768.             version += 1
  1769.         if version == 1:
  1770.             data['thumbURL'] = defaultFeedIconURL()
  1771.             version += 1
  1772.         if version == 2:
  1773.             data['lastViewed'] = datetime.min
  1774.             data['unwatched'] = 0
  1775.             data['available'] = 0
  1776.             version += 1
  1777.         assert(version == 3)
  1778.         data['updating'] = False
  1779.         data['visible'] = True
  1780.         self.__dict__ = data
  1781.         self.__class__ = RSSFeedImpl
  1782.